﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Objects;
using System.Linq;
using System.Reflection;
using System.Text;
using Equation.Enterprise.Framework.Domain.Model;

namespace Equation.Enterprise.Framework.Domain.Data
{
    /// <summary>
    /// Build DbContext using input Mapping Assembly
    /// It first adds all instances in mapping assemblies into Base (DbModelBuider) Configuration for the mappping
    /// </summary>
    public class DbContextBuilder : DbModelBuilder, IDbContextBuilder
    {
        protected readonly DbConnection DbConnection;
        protected readonly DbProviderFactory ProviderFactory;
        protected readonly ConnectionStringSettings ConnectionStringSettings;
        protected readonly bool LazyLoadEnabled;
        protected readonly DbCompiledModel DbCompiledModel;

        public DbContextBuilder(string connectionStringName, string[] mappingAssemblies, bool lazyLoadEnabled=true)
        {
            base.Conventions.Remove<IncludeMetadataConvention>();

            this.ConnectionStringSettings = ConfigurationManager.ConnectionStrings[connectionStringName];
            this.ProviderFactory = DbProviderFactories.GetFactory(ConnectionStringSettings.ProviderName);
            this.LazyLoadEnabled = lazyLoadEnabled;

            //Add Entity Configuration to Builder
            AddConfiguration(mappingAssemblies);

            this.DbConnection = ProviderFactory.CreateConnection();
            if (this.DbConnection != null)
                this.DbConnection.ConnectionString = ConnectionStringSettings.ConnectionString;
            this.DbCompiledModel = this.Build(this.DbConnection).Compile();
        }

        public DbContextBuilder(string connectionStringName, Assembly[] mappingAssemblies, bool lazyLoadEnabled = true)
        {
            base.Conventions.Remove<IncludeMetadataConvention>();

            this.ConnectionStringSettings = ConfigurationManager.ConnectionStrings[connectionStringName];
            this.ProviderFactory = DbProviderFactories.GetFactory(ConnectionStringSettings.ProviderName);
            this.LazyLoadEnabled = lazyLoadEnabled;

            //Add Entity Configuration to Builder
            AddConfiguration(mappingAssemblies);

            this.DbConnection = ProviderFactory.CreateConnection();
            if (this.DbConnection != null)
                this.DbConnection.ConnectionString = ConnectionStringSettings.ConnectionString;
            this.DbCompiledModel = this.Build(this.DbConnection).Compile();
        }

        private bool AddConfiguration(Assembly[] mappingAssemblies)
        {
            if (mappingAssemblies == null || mappingAssemblies.Length == 0)
                throw new ArgumentNullException("mappingAssemblies", "You must specify at lease one mapping assembly");

            bool hasMappingClass = false;
            foreach (Assembly mappingAssembly in mappingAssemblies)
            {
                foreach (Type type in mappingAssembly.GetTypes())
                {
                    if (!type.IsAbstract)
                    {
                        if (type.BaseType != null &&
                            type.BaseType.IsGenericType &&
                            (type.BaseType.GetGenericTypeDefinition() == typeof(EntityMapping<>) ||
                                type.BaseType.GetGenericTypeDefinition() == typeof(EntityMappingWithTypedId<,>)))
                        {
                            hasMappingClass = true;
                            dynamic configurationInstance = Activator.CreateInstance(type);
                            this.Configurations.Add(configurationInstance);
                        }
                    }
                }
            }

            return hasMappingClass;
        }

        /// <summary>
        /// Create instance for each type in mappingAssemblies and register them into base Configurations
        /// </summary>
        /// <param name="mappingAssemblies"></param>
        private bool AddConfiguration(string[] mappingAssemblies)
        {
            IList<Assembly> assemblies = new List<Assembly>();
            foreach (string assemblyName in mappingAssemblies)
            {
                Assembly assembly = Assembly.LoadFrom(BuildAssemlyName(assemblyName));
                assemblies.Add(assembly);
            }

            return AddConfiguration(assemblies.ToArray<Assembly>());
        }

        private string BuildAssemlyName(string assemblyName)
        {
            return (assemblyName.IndexOf("dll") == -1 ? assemblyName.Trim() : assemblyName.Trim() + ".dll");
        }

        public DbContext Build()
        {
            var objectContext = this.DbCompiledModel.CreateObjectContext<ObjectContext>(this.DbConnection);
            if (objectContext != null)
            {
                objectContext.ContextOptions.LazyLoadingEnabled = this.LazyLoadEnabled;
                return new DbContext(objectContext, false);
            }
            return null;
        }
    }
}
